/**
 * Copyright 2015 lyancoffee.com
 */
package com.xc.quickstart.common.util;

import cn.hutool.core.convert.Convert;
import org.springframework.data.geo.Point;

import java.awt.geom.Point2D;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GeoUtil {

    private static final double EARTH_RADIUS = 6370996.81;

    private static double rad(double d) {
        return d * Math.PI / 180.0;
    }

    public static int computeDistance(double lat1, double lng1, double lat2, double lng2) {
        int distance = computeDistanceInMeter(lat1, lng1, lat2, lng2);
        return (distance / 100) * 100;
    }

    public static int computeDistanceInMeter(double lat1, double lng1, double lat2, double lng2) {
        double radLat1 = rad(lat1);
        double radLat2 = rad(lat2);
        double a = radLat1 - radLat2;
        double b = rad(lng1) - rad(lng2);

        double s = 2 * Math.asin(Math.sqrt(
                Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
        s = s * EARTH_RADIUS;
        return (int) Math.round(s);
    }

    public static List<Map<String, Double>> splitPositionString(String positionsString) {
        List<Map<String, Double>> positionList = new ArrayList<Map<String, Double>>();
        String positions[] = positionsString.split(";");
        String temp = "";
        if (positions.length > 0) {
            for (String positionStr : positions) {
                if (positionStr.equals(temp)) {
                    continue;
                }
                temp = positionStr;
                String pos[] = positionStr.split(",");
                Map<String, Double> posTmpMap = new HashMap<String, Double>();
                posTmpMap.put("lat", Double.parseDouble(pos[1].trim()));
                posTmpMap.put("lng", Double.parseDouble(pos[0].trim()));
                positionList.add(posTmpMap);
            }
        }
        return positionList;
    }

    public static boolean isPointInPolygon(Double lng, Double lat, String positionsString) {
        Map<String, Double> point = new HashMap<>();
        point.put("lng", lng);
        point.put("lat", lat);
        return GeoUtil.isPointInPolygon(point, positionsString);
    }

    public static boolean isPointInPolygon(Map<String, Double> point, String positionsString) {
        List<Map<String, Double>> polygon = splitPositionString(positionsString);
        int polygonSize = polygon.size();
        boolean boundOrVertex = true;
        int intersectCount = 0;
        double precision = 2e-10;
        Map<String, Double> p1, p2;

        p1 = polygon.get(0);
        for (int i = 1; i <= polygonSize; ++i) {
            if (point.equals(p1)) {
                return boundOrVertex;
            }

            p2 = polygon.get(i % polygonSize);
            if (point.get("lat") < Math.min(p1.get("lat"), p2.get("lat"))
                    || point.get("lat") > Math.max(p1.get("lat"), p2.get("lat"))) {
                p1 = p2;
                continue;
            }

            if (point.get("lat") > Math.min(p1.get("lat"), p2.get("lat"))
                    && point.get("lat") < Math.max(p1.get("lat"), p2.get("lat"))) {
                if (point.get("lng") <= Math.max(p1.get("lng"), p2.get("lng"))) {
                    if (p1.get("lat") == p2.get("lat") && point.get("lng") >= Math.min(p1.get("lng"), p2.get("lng"))) {
                        return boundOrVertex;
                    }

                    if (p1.get("lng") == p2.get("lng")) {
                        if (p1.get("lng") == point.get("lng")) {
                            return boundOrVertex;
                        } else {
                            ++intersectCount;
                        }
                    } else {
                        double xinters = (point.get("lat") - p1.get("lat")) * (p2.get("lng") - p1.get("lng"))
                                / (p2.get("lat") - p1.get("lat")) + p1.get("lng");
                        if (Math.abs(point.get("lng") - xinters) < precision) {
                            return boundOrVertex;
                        }

                        if (point.get("lng") < xinters) {
                            ++intersectCount;
                        }
                    }
                }
            } else {
                if (point.get("lat") == p2.get("lat") && point.get("lng") <= p2.get("lng")) {
                    Map<String, Double> p3 = polygon.get((i + 1) % polygonSize);
                    if (point.get("lat") >= Math.min(p1.get("lat"), p3.get("lat"))
                            && point.get("lat") <= Math.max(p1.get("lat"), p3.get("lat"))) {
                        ++intersectCount;
                    } else {
                        intersectCount += 2;
                    }
                }
            }
            p1 = p2;
        }

        if (intersectCount % 2 == 0) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * 计算多边形面积, 返回值单位为 平方米
     *
     * @param polygon 有序顶点列表
     */
    public static double calculateArea(List<Map<String, Double>> polygon) {
        if (polygon.size() < 3) {
            return 0l;
        }
        double totalArea = 0;
        double lowX = 0.0;
        double LowY = 0.0;
        double MiddleX = 0.0;
        double MiddleY = 0.0;
        double HighX = 0.0;
        double HighY = 0.0;
        double AM = 0.0;
        double BM = 0.0;
        double CM = 0.0;
        double AL = 0.0;
        double BL = 0.0;
        double CL = 0.0;
        double AH = 0.0;
        double BH = 0.0;
        double CH = 0.0;
        double CoefficientL = 0.0;
        double CoefficientH = 0.0;
        double ALtangent = 0.0;
        double BLtangent = 0.0;
        double CLtangent = 0.0;
        double AHtangent = 0.0;
        double BHtangent = 0.0;
        double CHtangent = 0.0;
        double ANormalLine = 0.0;
        double BNormalLine = 0.0;
        double CNormalLine = 0.0;
        double OrientationValue = 0.0;
        double AngleCos = 0.0;
        double Sum1 = 0.0;
        double Sum2 = 0.0;
        double Count2 = 0;
        double Count1 = 0;
        double Sum = 0.0;
        double Radius = EARTH_RADIUS;
        int Count = polygon.size();
        for (int i = 0; i < Count; i++) {
            if (i == 0) {
                lowX = polygon.get(Count - 1).get("lng") * Math.PI / 180;
                LowY = polygon.get(Count - 1).get("lat") * Math.PI / 180;
                MiddleX = polygon.get(0).get("lng") * Math.PI / 180;
                MiddleY = polygon.get(0).get("lat") * Math.PI / 180;
                HighX = polygon.get(1).get("lng") * Math.PI / 180;
                HighY = polygon.get(1).get("lat") * Math.PI / 180;
            } else if (i == Count - 1) {
                lowX = polygon.get(Count - 2).get("lng") * Math.PI / 180;
                LowY = polygon.get(Count - 2).get("lat") * Math.PI / 180;
                MiddleX = polygon.get(Count - 1).get("lng") * Math.PI / 180;
                MiddleY = polygon.get(Count - 1).get("lat") * Math.PI / 180;
                HighX = polygon.get(0).get("lng") * Math.PI / 180;
                HighY = polygon.get(0).get("lat") * Math.PI / 180;
            } else {
                lowX = polygon.get(i - 1).get("lng") * Math.PI / 180;
                LowY = polygon.get(i - 1).get("lat") * Math.PI / 180;
                MiddleX = polygon.get(i).get("lng") * Math.PI / 180;
                MiddleY = polygon.get(i).get("lat") * Math.PI / 180;
                HighX = polygon.get(i + 1).get("lng") * Math.PI / 180;
                HighY = polygon.get(i + 1).get("lat") * Math.PI / 180;
            }
            AM = Math.cos(MiddleY) * Math.cos(MiddleX);
            BM = Math.cos(MiddleY) * Math.sin(MiddleX);
            CM = Math.sin(MiddleY);
            AL = Math.cos(LowY) * Math.cos(lowX);
            BL = Math.cos(LowY) * Math.sin(lowX);
            CL = Math.sin(LowY);
            AH = Math.cos(HighY) * Math.cos(HighX);
            BH = Math.cos(HighY) * Math.sin(HighX);
            CH = Math.sin(HighY);
            CoefficientL = (AM * AM + BM * BM + CM * CM) / (AM * AL + BM * BL + CM * CL);
            CoefficientH = (AM * AM + BM * BM + CM * CM) / (AM * AH + BM * BH + CM * CH);
            ALtangent = CoefficientL * AL - AM;
            BLtangent = CoefficientL * BL - BM;
            CLtangent = CoefficientL * CL - CM;
            AHtangent = CoefficientH * AH - AM;
            BHtangent = CoefficientH * BH - BM;
            CHtangent = CoefficientH * CH - CM;
            AngleCos = (AHtangent * ALtangent + BHtangent * BLtangent + CHtangent * CLtangent)
                    / (Math.sqrt(AHtangent * AHtangent + BHtangent * BHtangent + CHtangent * CHtangent)
                    * Math.sqrt(ALtangent * ALtangent + BLtangent * BLtangent + CLtangent * CLtangent));
            AngleCos = Math.acos(AngleCos);
            ANormalLine = BHtangent * CLtangent - CHtangent * BLtangent;
            BNormalLine = 0 - (AHtangent * CLtangent - CHtangent * ALtangent);
            CNormalLine = AHtangent * BLtangent - BHtangent * ALtangent;
            if (AM != 0)
                OrientationValue = ANormalLine / AM;
            else if (BM != 0)
                OrientationValue = BNormalLine / BM;
            else
                OrientationValue = CNormalLine / CM;
            if (OrientationValue > 0) {
                Sum1 += AngleCos;
                Count1++;
            } else {
                Sum2 += AngleCos;
                Count2++;
            }
        }
        double tempSum1, tempSum2;
        tempSum1 = Sum1 + (2 * Math.PI * Count2 - Sum2);
        tempSum2 = (2 * Math.PI * Count1 - Sum1) + Sum2;
        if (Sum1 > Sum2) {
            if ((tempSum1 - (Count - 2) * Math.PI) < 1)
                Sum = tempSum1;
            else
                Sum = tempSum2;
        } else {
            if ((tempSum2 - (Count - 2) * Math.PI) < 1)
                Sum = tempSum2;
            else
                Sum = tempSum1;
        }
        totalArea = (Sum - (Count - 2) * Math.PI) * Radius * Radius;

        return totalArea;
    }

    /**
     * 判断点是否在多边形内
     *
     * @param point 检测点
     * @param pts   多边形的顶点
     * @return 点在多边形内返回true, 否则返回false
     */
    public static boolean IsPtInPoly(Point2D.Double point, List<Point2D.Double> pts) {

        int N = pts.size();
        boolean boundOrVertex = true; //如果点位于多边形的顶点或边上，也算做点在多边形内，直接返回true
        int intersectCount = 0;//cross points count of x
        double precision = 2e-10; //浮点类型计算时候与0比较时候的容差
        Point2D.Double p1, p2;//neighbour bound vertices
        Point2D.Double p = point; //当前点

        p1 = pts.get(0);//left vertex
        for (int i = 1; i <= N; ++i) {//check all rays
            if (p.equals(p1)) {
                return boundOrVertex;//p is an vertex
            }

            p2 = pts.get(i % N);//right vertex
            if (p.x < Math.min(p1.x, p2.x) || p.x > Math.max(p1.x, p2.x)) {//ray is outside of our interests
                p1 = p2;
                continue;//next ray left point
            }

            if (p.x > Math.min(p1.x, p2.x) && p.x < Math.max(p1.x, p2.x)) {//ray is crossing over by the algorithm (common part of)
                if (p.y <= Math.max(p1.y, p2.y)) {//x is before of ray
                    if (p1.x == p2.x && p.y >= Math.min(p1.y, p2.y)) {//overlies on a horizontal ray
                        return boundOrVertex;
                    }

                    if (p1.y == p2.y) {//ray is vertical
                        if (p1.y == p.y) {//overlies on a vertical ray
                            return boundOrVertex;
                        } else {//before ray
                            ++intersectCount;
                        }
                    } else {//cross point on the left side
                        double xinters = (p.x - p1.x) * (p2.y - p1.y) / (p2.x - p1.x) + p1.y;//cross point of y
                        if (Math.abs(p.y - xinters) < precision) {//overlies on a ray
                            return boundOrVertex;
                        }

                        if (p.y < xinters) {//before ray
                            ++intersectCount;
                        }
                    }
                }
            } else {//special case when ray is crossing through the vertex
                if (p.x == p2.x && p.y <= p2.y) {//p crossing over p2
                    Point2D.Double p3 = pts.get((i + 1) % N); //next vertex
                    if (p.x >= Math.min(p1.x, p3.x) && p.x <= Math.max(p1.x, p3.x)) {//p.x lies between p1.x & p3.x
                        ++intersectCount;
                    } else {
                        intersectCount += 2;
                    }
                }
            }
            p1 = p2;//next ray left point
        }

        if (intersectCount % 2 == 0) {//偶数在多边形外
            return false;
        } else { //奇数在多边形内
            return true;
        }

    }

    /**
     * 获取点与多边形之间最近距离
     * @param point
     * @param points
     * @return 0.0 ：点位于多边形内部  >0.0 : 点与多边形之间的最近距离
     */
    public static double pintoToPolygonMinDist(Point point, List<Point> points) {
        double dist = Double.MAX_VALUE;
        int N = points.size();
        for (int i = 0; i < N; i++) {
            int j = i +1;
            if(i == (N-1)){
                j = 0;
            }
            double distance = getDistanceByLineSegment(point, points.get(i), points.get(j));
            dist = Math.min(dist, distance);
        }
        return dist;
    }

    /**
     * 点到线段的最短距离
     * 运用向量计算三角形是锐角还是钝角
     * 两种情况
     * 1、锐角三角形运用海伦公式来计算最短距离
     * 2、钝角三角形需要区分到A还是B点是钝角，那边是钝角，则对应边长为最短距离
     * @param point 点位 起点
     * @param pointA 线段A点 末点
     * @param pointB 线段B点  末点
     * @return
     */
    public static double getDistanceByLineSegment(Point point,Point pointA,Point pointB){
        if(calculateAngle(pointA,point,pointB).compareTo(Convert.toBigDecimal(0))<=0){
            return computeDistanceInMeterDouble(point,pointA);
        }
        if(calculateAngle(pointB,point,pointA).compareTo(Convert.toBigDecimal(0))<=0){
            return computeDistanceInMeterDouble(point,pointB);
        }
        return getDistanceByPointToLine(point,pointA,pointB);
    }

    /**
     * 计算两点间的距离，单位：米
     */
    public static double computeDistanceInMeterDouble(Point p1, Point p2) {
        double radLat1 = rad(p1.getX());
        double radLat2 = rad(p2.getX());
        double a = radLat1 - radLat2;
        double b = rad(p1.getY()) - rad(p2.getY());

        double s = 2 * Math.asin(Math.sqrt(
                Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
        s = s * EARTH_RADIUS;
        return  s;
    }
    /**
     * 点到直线的距离
     */
    public static double getDistanceByPointToLine(Point point, Point pnt1, Point pnt2){
        // 点到直线的最短距离的判断 三角形面积。海伦-秦九韶公式 s = 开根(p(p-a)(p-b)(p-c))
        double space = 0;
        double a, b, c;
        a = computeDistanceInMeter(pnt1, pnt2);// 线段的长度
        b = computeDistanceInMeter(pnt1, point);// (x1,y1)到点的距离
        c = computeDistanceInMeter(pnt2, point);// (x2,y2)到点的距离
        if (c <= 0.000001 || b <= 0.000001) {
            space = 0;
            return space;
        }
        if (a <= 0.000001) {
            space = b;
            return space;
        }
        if (c * c >= a * a + b * b) {
            space = b;
            return space;
        }
        if (b * b >= a * a + c * c) {
            space = c;
            return space;
        }
        double p = (a + b + c) / 2;// 半周长
        double s = Math.sqrt(p * (p - a) * (p - b) * (p - c));// 海伦公式求面积
        space = 2 * s / a;// 返回点到线的距离（利用三角形面积公式求高）
        return space;
    }

    /**
     * 计算两点间的距离，单位：米
     */
    public static int computeDistanceInMeter(Point p1, Point p2) {
        double radLat1 = rad(p1.getX());
        double radLat2 = rad(p2.getX());
        double a = radLat1 - radLat2;
        double b = rad(p1.getY()) - rad(p2.getY());

        double s = 2 * Math.asin(Math.sqrt(
                Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
        s = s * EARTH_RADIUS;
        return (int) Math.round(s);
    }

    /**
     * 向量计算角度   >0 锐角 =0直角 <0 钝角
     * @param start 起点
     * @param endA  向量终点A
     * @param endB  向量终点B
     * @return
     */
    private static BigDecimal calculateAngle(Point start, Point endA, Point endB){
        Point vectorA=new Point(endA.getX()-start.getX(), endA.getY()-start.getY());
        Point vectorB=new Point(endB.getX()-start.getX(), endB.getY()-start.getY());
        BigDecimal cal= calculateVectorMulti(vectorA,vectorB);
        return cal;
    }

    /**
     *向量相乘  >0 锐角 =0直角 <0 钝角
     * @param vectorA
     * @param vectorB
     * @return
     */
    private static BigDecimal  calculateVectorMulti(Point vectorA,Point vectorB){
        return  Convert.toBigDecimal(vectorA.getX()).multiply(Convert.toBigDecimal(vectorB.getX()))
                .add(Convert.toBigDecimal(vectorA.getY()).multiply(Convert.toBigDecimal(vectorB.getY())));
    }
}
